iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

從 Vibe Coding 到可維護專案:用 Hermes Agent 重讀 RoomRush Android 專題系列 第 14 篇

Day 14 | CSV 檔案是怎麼變成 App 裡可以查的課表資料?

  • 分享至 

  • xImage
  •  

前言:SF.csv / ES.csv 如何被匯入 Room Database?

前一天我們理解了 ClassroomScheduleRepository.kt如何包裝 DAO,讓 ViewModel 不直接接觸資料庫操作。今天我們要順著資料流繼續往下看核心資料庫的設定— AppDatabase.kt。

專案中的 SF.csv 與 ES.csv,是我們預先整理好的 114 學年度下學期大樓課表原始檔。然而,靜態 CSV 無法直接被 Room 操作,必須在資料庫初始化時解析並轉換為 ClassroomSchedule 實體(Entity)寫入資料庫。今天就來聊聊如何在 AppDatabase 中實現這個預填資料的流程。


一開始我認為

一開始我對:「因為原始課表資料無法直接被 Room 操作,必須另外的檔案把這些 CSV 檔案放進去」,這部分的概念上大致理解:CSV 只是文字檔,Room 是 SQLite 的封裝,兩者中間需要一層「解析器(Parser)」把純文字轉換為 ClassroomSchedule 的 Entity 物件。

但當時我對具體運作仍有盲點:

  1. 匯入的時間點:
    我一開始以為是在 MainActivity 啟動時手動寫程式碼去讀檔。但重讀後發現,專案是利用 RoomDatabase.Callback 將匯入邏輯綁定在資料庫自身的生命週期中。

  2. onCreate 與 onOpen 的觸發機制:
    原本以為只要靠 onCreate(資料庫首次建立時)預載一次就好;但回頭看程式碼才發現,我們連 onOpen(每次資料庫被開啟時)都呼叫了 triggerPopulate();配合 DAO 的 REPLACE 策略,等於每次開啟 App 都會重新對齊最新 CSV 資料。

  3. 非同步與執行緒安全:
    讀取檔案與寫入數十間教室課表屬於 I/O 密集操作,若在主執行緒執行會造成畫面卡頓。重讀後發現專案使用了 CoroutineScope(Dispatchers.IO) 切換至背景執行緒,確保了 UI 的流暢度。


實際讀完後,AppDatabase.kt 負責什麼

和 Hermes Agent 一起重讀檔案後,我更知道AppDatabase.kt 是 RoomRush 的 Room Database 設定中心;它除了負責定義資料庫、還提供 DAO、建立資料庫單例,更重要的是把 assets 裡的 SF.csv 和 ES.csv 解析成 ClassroomSchedule 後寫入資料庫。

實際剖析程式碼後,重點核心可以分為以下四點:

1. 提供 DAO

abstract fun classroomScheduleDao(): ClassroomScheduleDao
  • 技術細節:這個抽象方法讓 AppDatabase 可以提供 ClassroomScheduleDao,後續 Repository 會透過這個 DAO 存取資料。

2. 單例(Singleton)建立資料庫

@Volatile
private var INSTANCE: AppDatabase? = null

fun getInstance(context: Context): AppDatabase {
    return INSTANCE ?: synchronized(this) {
        val instance = Room.databaseBuilder(
            context.applicationContext,
            AppDatabase::class.java,
            "classroom_database"
        )
            .fallbackToDestructiveMigration()
            .addCallback(DatabaseCallback(context))
            .build()
        INSTANCE = instance
        instance
    }
}
  • 技術細節:使用 @Volatile 與 synchronized(this) 確保多執行緒下,整個 App 只建立一個資料庫實例(Singleton),避免重複建立耗費資源。

3. 生命週期監聽與非同步預載(DatabaseCallback & Coroutines)

private class DatabaseCallback(private val context: Context) : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        triggerPopulate()
    }

    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
        triggerPopulate()
    }

    private fun triggerPopulate() {
        CoroutineScope(Dispatchers.IO).launch {
            val dao = getInstance(context).classroomScheduleDao()
            prePopulateDatabase(context, dao)
        }
    }
}
  • 技術細節:透過 RoomDatabase.Callback 監聽資料庫生命週期—onCreate(資料庫初次建立)與 onOpen(資料庫每次開啟)。由於讀寫檔案屬於 I/O 密集型工作,特別使用 CoroutineScope(Dispatchers.IO) 切換至背景執行緒,避免主畫面卡住。

4. CSV 課表解析與批次寫入(Pre-population)

private suspend fun prePopulateDatabase(context: Context, dao: ClassroomScheduleDao) {
    val sfSchedules = parseCsv(context, "SF.csv")
    val esSchedules = parseCsv(context, "ES.csv")

    if (sfSchedules.isNotEmpty() || esSchedules.isNotEmpty()) {
        dao.insertAll(sfSchedules + esSchedules)
    }
}
  • 技術細節:讀取 assets 裡的 SF.csv 與 ES.csv,利用 drop(1) 跳過表頭並依照欄位順序解析為 ClassroomSchedule 物件清單。最後呼叫 dao.insertAll() 進行批次寫入(搭配 OnConflictStrategy.REPLACE 確保資料覆蓋為最新狀態)。

它接在哪一條流程上

App 啟動
    ↓
建立 AppDatabase
    ↓
取得 ClassroomScheduleDao
    ↓
讀取 SF.csv / ES.csv
    ↓
parseCsv()
    ↓
每一列 CSV → ClassroomSchedule
    ↓
dao.insertAll()
    ↓
Room Database

Hermes Agent 幫我檢查出的重點

AppDatabase 是 RoomRush 的 Room Database 設定中心。

  • 它透過 @Database 指定 ClassroomSchedule 為 Entity,提供 classroomScheduleDao() 讓 Repository 可以取得 DAO,並用 Singleton 確保 App 只建立一個資料庫實例。
  • 資料庫建立或開啟時,DatabaseCallback 會觸發 triggerPopulate(),在 Dispatchers.IO 背景執行緒讀取 SF.csv 和 ES.csv。
  • parseCsv() 會略過 CSV 表頭,把每一列依照欄位順序轉成 ClassroomSchedule。
  • 最後用 dao.insertAll() 批次寫入資料庫。

這個檔案帶出的維護觀察

  1. onOpen 重複載入覆蓋資料: 每次打開資料庫時,都可能重新讀取 CSV 並寫入,如果使用者或管理者功能 修改過資料,下次開啟資料庫時可能 被 CSV 覆蓋,導致舊檔案的損失。
  2. 依賴 CSV 欄位順序: 程式靠固定的索引位置(Index)取值,只要檔案欄位順序稍微更動,整批資料就會對錯欄位。
  3. catch 吞掉錯誤直接傳空清單: 雖然表面上避免了 App 閃退,但就像把火警警報器關掉一樣,出問題時開發者完全找不到背後真實的錯誤原因。

重構的核心,在於將預載改移至 onCreate 防止資料被洗掉;讀檔時可以先抓取第一列欄位名稱,建立索引對照表,讓 CSV 標頭名稱可以動態對齊欄位;並改用 Result 清楚回報錯誤而非默默吞掉。


小結

讀到 AppDatabase 時,我終於把 RoomRush 的資料來源串起來了。

前面看到的 ClassroomSchedule、DAO 和 Repository,都是資料層的一部分;而 AppDatabase 則是把這些組合起來的資料庫中心。它不只建立 Room Database,還會在資料庫建立或開啟時,把 assets 裡的 CSV 讀進來,轉成 ClassroomSchedule 後寫入資料庫。

這個檔案也讓我看到幾個正式 App 需要注意的地方:例如 onOpen 每次開啟資料庫都會預載 CSV,如果管理者曾修改資料,可能被 CSV 覆蓋。另外,CSV 解析非常依賴欄位順序,只要欄位順序改變,資料就可能對錯欄位。最後,解析錯誤時直接回傳空清單雖然可以避免崩潰,但也可能讓錯誤被隱藏。這些都可以列為後續技術債觀察。

用一句話總結AppDatabase:

AppDatabase 建立 Room Database,提供 DAO,並在資料庫建立 / 開啟時把 SF.csv 和 ES.csv 解析成 ClassroomSchedule 寫入資料庫。


下一篇預告:Repository 從哪裡生出來?我第一次看懂 AppContainer 的用途

當我們把資料庫(Entity、DAO、Database)與畫面邏輯都串起來之後,下一個我們會介紹到:這些物件到底是誰負責「生」出來的?

ViewModel 需要 Repository,Repository 需要 DAO,而 DAO 又必須從 Database 取得;如果每個頁面都自己 new 一次,系統資源很快就會被用光。這時候就是 依賴注入(Dependency Injection) 就派上用場了。

下一篇,我們將解密 EmptyRoomFinderApp.kt 與 AppContainer.kt:看 App 如何打造一個「全域工具箱」,用 Context 依序組裝好資料庫(AppDatabase)與倉庫(ClassroomScheduleRepository),讓所有頁面都能隨取隨用。


上一篇
Day 13 | 為什麼 ViewModel 不該直接碰資料庫?Repository 的角色是什麼?
下一篇
Day 15 | Repository 從哪裡生出來?我第一次看懂 AppContainer 的用途
系列文
從 Vibe Coding 到可維護專案:用 Hermes Agent 重讀 RoomRush Android 專題 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言